import os
import re
import concurrent.futures
from dataclasses import dataclass
from typing import Dict, List, Tuple

import requests
import bs4
from packaging.version import Version

from spd_launcher.config import GlobalConfig
from spd_launcher.utils import PathUtils, str_is_empty

class DownloadFail(Exception):
    
    def __init__(self, *args):
        super().__init__(*args)
    
@dataclass    
class Chunk:
    
    start : int
    end : int
    size : int
    
    

class AutoSpdUpdater:
    
    # TODO 用对象传参
    def __init__(self, config : GlobalConfig) -> None:
        self.config = config
        
        self.threads_num = self.config.theards_num
        
        if self.threads_num.value() > 32 or self.threads_num.value() < 0:
            raise DownloadFail("配置线程数不在[1-8]区间内")
            
        self.temp_path = self.config.temp_path
        
        self.url_list_domain = self.config.url_list_domain
        self.current_url_index_domain = self.config.current_url_index_domain
        self.current_domain = None
        
        try:
            self.current_domain = self.url_list_domain.value()[
                self.current_url_index_domain.value()
            ]
        except IndexError:
            raise DownloadFail(f"配置的当前域名地址索引{self.current_url_index_domain.value()}不存在")
        
        if str_is_empty(self.current_domain):
            raise DownloadFail("当前域名为空") 
        
        self.version_api = self.config.versions_api
        self.realse_api = self.config.realse_api
        self.version_pattern_api = self.config.version_pattern_api
         
        self.root_path = self.config.root_path
        self.version_path = self.config.versions_path
        
    def load_version_list(self) -> List[str]:
        res = requests.get(self.tag_api)
        # pdb.set_trace()
        if res.ok:
            soup = bs4.BeautifulSoup(res.text, features="html.parser")
            result_text = soup.text
            
            return sorted(set(re.findall(self.version_pattern_api.value(), result_text)), key=Version, reverse=True)
            
        
    def download_game(self, version : str) -> None:
        if not re.match(self.version_pattern_api.value(), version):
            raise DownloadFail()
        
        game_url = self.__build_game_url(version)
        
        if str_is_empty(game_url):
            raise DownloadFail("游戏下载路径为空")
        
        resource_head_res = requests.head(game_url)
        
        if resource_head_res.status_code != 302:
            raise DownloadFail("资源不存在")
        
        if not "location" in resource_head_res.headers.keys():
            raise DownloadFail("目标地址不存在")
        
        location_url = resource_head_res.headers["location"]
        
        if str_is_empty(location_url):
            raise DownloadFail("目标地址为空")
        
        location_head_res = requests.head(location_url)
        if not location_head_res.ok:
            raise DownloadFail("下载失败，无法请求头部")
        
        if not "Content-Length" in resource_head_res.headers.keys():
            raise DownloadFail("下载失败，文件大小加载失败")
        file_size = int(location_head_res.headers["Content-Length"])
        
        
        with concurrent.futures.ThreadPoolExecutor(max_workers=self.threads_num.value()) as executor:
            
            future_to_chunk = {}
            chunks = self.file_size_to_chunks(file_size, self.threads_num.value())
            for i in range(len(chunks)):
                fut = executor.submit(self.download_chunk, i , location_url, version, chunks[i])
                future_to_chunk[fut] = i
            
            for fut in concurrent.futures.as_completed(future_to_chunk):
                idx = future_to_chunk[fut]
                try:
                    fut.result()          # 如果 download_chunk 抛了异常，这里会重新抛出
                except DownloadFail as e:
                    print(f'块 {idx} 下载失败: {e}')
                    
        self.merge_chunks(version)
            
    
    def download_chunk(self, index : int, url: str, version : str,  chunk : Chunk) -> None:
        stream_size = 64 * 1024 # 64K
        file_path = self.__build_chunk_temp_path(version, index)

        if os.path.exists(file_path):
            if os.path.getsize(file_path) == chunk.size:
                return
            os.remove(file_path)
        else:
            os.makedirs(os.path.dirname(file_path), exist_ok=True)
            
        headers = None
        
        try:
            headers = self.__build_range_header(chunk)
        except ValueError:
            raise DownloadFail("构造请求头失败")
        
        try:
            if headers is None or headers == {}:
                raise DownloadFail()
            res = requests.get(url, headers=headers, stream=True, timeout=30)
            res.raise_for_status()
            with open(file_path, "wb") as f:
                for data in res.iter_content(chunk_size=stream_size):
                    if data:
                        f.write(data)
        except requests.RequestException as e:
            raise DownloadFail("下载失败", e)
        
            
          
    @staticmethod
    def file_size_to_chunks(file_size : int, chunk_num : int) -> List[Chunk | None]:
        if chunk_num <= 0:
            raise ValueError('chunk_num 必须为正整数')

        chunk_size = file_size // chunk_num
        
        if chunk_size == 0:
            chunk_size = 1
            chunk_num = min(chunk_num, file_size)

        chunks = []
        start = 0
        for _ in range(chunk_num - 1):
            end = start + chunk_size
            chunk = Chunk(start, end, chunk_size)
            chunks.append(chunk)
            start = end + 1
            
        chunks.append(Chunk(start, file_size, file_size - start))

        return chunks
    
    def merge_chunks(self, version : str, buff_size: int = 1024*1024):
        chunk_temp_paths = []
        output_path = PathUtils.link_work_path(self.root_path.value(), self.version_path.value(), version, f"{version}.jar")
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        
        for i in range(self.threads_num.value()):
            chunk_temp_paths.append(self.__build_chunk_temp_path(version, i))
        
        
        
        with open(output_path, 'wb') as dst:
            for path in chunk_temp_paths:
                with open(path, 'rb') as src:
                    while True:
                        data = src.read(buff_size)
                        if not data:
                            break
                        dst.write(data)
                        
    def __build_chunk_temp_path(self, version , index):
        file_name = f"spd{version}.chunk{index}"
        return PathUtils.link_work_path(
            self.root_path.value(), self.temp_path.value(), version, file_name
        )
        
     
    
    def __build_game_url(self, version : str) -> str:
        if re.match(self.version_pattern_api.value(), version) is None:
            raise ValueError(f"游戏版本:{version}不符合[d.d.d]的格式")
        return f"{self.current_domain}{self.realse_api.value().format(version)}" 
    
    @staticmethod
    def __build_range_header(c : Chunk) -> str:
        if c.start > c.end:
            raise ValueError()
        
        return {"Range" : f"bytes={c.start}-{c.end}"}
        

if __name__ == "__main__":
    from pprint import pprint
    auto = AutoSpdUpdater(GlobalConfig())
    auto.download_game("3.1.1")